package com.hqd.ch03.v23.aop.framework.aspectj;

import com.hqd.ch03.v23.core.ParameterNameDiscoverer;
import com.hqd.ch03.v23.utils.AspectJProxyUtils;
import lombok.Getter;
import lombok.Setter;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

@Setter
@Getter
public class AspectJAdviceParameterNameDiscoverer implements ParameterNameDiscoverer {
    public static final String THIS_JOIN_POINT = "thisJoinPoint";
    public static final String THIS_JOIN_POINT_STATIC_PART = "thisJoinPointStaticPart";

    private static final int STEP_JOIN_POINT_BINDING = 1;
    private static final int STEP_THROWING_BINDING = 2;
    private static final int STEP_RETURNING_BINDING = 3;
    private static final int STEP_PRIMITIVE_ARGS_BINDING = 4;
    private static final int STEP_ARGS_BINDING = 5;
    private static final int STEP_FINISHED = 6;


    /**
     * 切点表达式
     */
    private String pointcutExpression;

    /**
     * 是否抛出异常
     */
    private boolean raiseExceptions;

    /**
     * 返回值名
     */
    private String returningName;

    /**
     * 异常名
     */
    private String throwingName;

    /**
     * 参数类型
     */
    private Class<?>[] argumentTypes = new Class<?>[0];

    /**
     * 参数名
     */
    private String[] parameterNameBindings = new String[0];

    /**
     * 未绑定参数个数
     */
    private int numberOfRemainingUnboundArguments;

    public AspectJAdviceParameterNameDiscoverer(String pointcutExpression) {
        this.pointcutExpression = pointcutExpression;
    }

    @Override
    public String[] getParameterNames(Method method) {
        this.argumentTypes = method.getParameterTypes();
        this.numberOfRemainingUnboundArguments = this.argumentTypes.length;
        this.parameterNameBindings = new String[this.numberOfRemainingUnboundArguments];

        /**
         * 检验参数长度
         */
        int minimumNumberUnboundArgs = 0;
        if (this.returningName != null) {
            minimumNumberUnboundArgs++;
        }
        if (this.throwingName != null) {
            minimumNumberUnboundArgs++;
        }
        if (this.numberOfRemainingUnboundArguments < minimumNumberUnboundArgs) {
            throw new IllegalStateException(
                    "Not enough arguments in method to satisfy binding of returning and throwing variables");
        }
        try {
            int algorithmicStep = STEP_JOIN_POINT_BINDING;
            while ((this.numberOfRemainingUnboundArguments > 0) && algorithmicStep < STEP_FINISHED) {
                switch (algorithmicStep++) {
                    /**
                     * JoinPoint参数
                     */
                    case STEP_JOIN_POINT_BINDING:
                        /**
                         * JoinPoint.StaticPart参数
                         */
                        if (!maybeBindThisJoinPoint()) {
                            maybeBindThisJoinPointStaticPart();
                        }
                        break;
                    /**
                     * 异常增强时，异常参数
                     */
                    case STEP_THROWING_BINDING://joinpoincut
                        maybeBindThrowingVariable();
                        break;
                    case STEP_RETURNING_BINDING://返回值
                        maybeBindReturningVariable();
                        break;
                    case STEP_PRIMITIVE_ARGS_BINDING://基础类型
                        maybeBindPrimitiveArgsFromPointcutExpression();
                        break;
                    case STEP_ARGS_BINDING://args
                        maybeBindArgsFromPointcutExpression();
                        break;
                    default:
                        throw new IllegalStateException("Unknown algorithmic step: " + (algorithmicStep - 1));
                }
            }
        } catch (RuntimeException ex) {
            /**
             * 是否要抛出异常
             */
            if (this.raiseExceptions) {
                throw ex;
            } else {
                return null;
            }
        }
        /**
         * 参数是否全部匹配
         */
        if (this.numberOfRemainingUnboundArguments == 0) {
            return this.parameterNameBindings;
        } else {
            if (this.raiseExceptions) {
                throw new IllegalStateException("Failed to bind all argument names: " +
                        this.numberOfRemainingUnboundArguments + " argument(s) could not be bound");
            } else {
                return null;
            }
        }
    }

    private void maybeBindArgsFromPointcutExpression() {
        if (this.numberOfRemainingUnboundArguments > 1) {
            throw new RuntimeException("args内的类型参数不能超过1个。。");
        }
        if (this.numberOfRemainingUnboundArguments == 1) {
            List<String> varNames = new ArrayList<>();
            String[] tokens = this.pointcutExpression.split(" ");
            for (int i = 0; i < tokens.length; i++) {
                String token = tokens[i];
                if (token.equals("args") || token.startsWith("args(")) {
                    PointcutBody body = getPointcutBody(tokens, i);
                    /**
                     * 继续向后匹配，防止有漏网之鱼
                     */
                    i += body.numTokensConsumed;
                    List<String> candidateVarNames = new ArrayList<>();
                    maybeExtractVariableNamesFromArgs(body.text, candidateVarNames);
                    for (String varName : candidateVarNames) {
                        if (!alreadyBound(varName)) {
                            varNames.add(varName);
                        }
                    }
                }
            }
            /**
             * 没有匹配到对于变量
             */
            if (varNames.isEmpty()) {
                throw new RuntimeException("args未找到匹配参数。。");
            } else if (varNames.size() == 1) {
                for (int i = 0; i < this.argumentTypes.length; i++) {
                    if (isUnbound(i) && !this.argumentTypes[i].isPrimitive()) {
                        bindParameterName(i, varNames.get(0));
                        break;
                    }
                }
            } else {
                throw new RuntimeException("args匹配到多余的参数。。");
            }
        }
    }

    private void maybeBindPrimitiveArgsFromPointcutExpression() {
        int numUnboundPrimitives = countNumberOfUnboundPrimitiveArguments();
        if (numUnboundPrimitives > 1) {
            throw new RuntimeException("args内的基础类型参数不能超过1个。。");
        }
        if (numUnboundPrimitives == 1) {
            List<String> varNames = new ArrayList<>();
            String[] tokens = this.pointcutExpression.split(" ");
            for (int i = 0; i < tokens.length; i++) {
                String token = tokens[i];
                if (token.equals("args") || token.startsWith("args(")) {
                    PointcutBody body = getPointcutBody(tokens, i);
                    /**
                     * 继续向后匹配，防止有漏网之鱼
                     */
                    i += body.numTokensConsumed;
                    maybeExtractVariableNamesFromArgs(body.text, varNames);
                }
            }
            /**
             * 没有匹配到对于变量
             */
            if (varNames.isEmpty()) {
                throw new RuntimeException("args未找到匹配参数。。");
            } else if (varNames.size() == 1) {
                for (int i = 0; i < this.argumentTypes.length; i++) {
                    if (isUnbound(i) && this.argumentTypes[i].isPrimitive()) {
                        bindParameterName(i, varNames.get(0));
                        break;
                    }
                }
            } else {
                throw new RuntimeException("args匹配到多余的参数。。");
            }
        }
    }

    private void maybeExtractVariableNamesFromArgs(String argsSpec, List<String> varNames) {
        if (argsSpec == null) {
            return;
        }
        String[] tokens = argsSpec.split(",");
        for (int i = 0; i < tokens.length; i++) {
            tokens[i] = tokens[i].trim();
            String varName = maybeExtractVariableName(tokens[i]);
            if (varName != null) {
                varNames.add(varName);
            }
        }
    }

    private boolean alreadyBound(String varName) {
        for (int i = 0; i < this.parameterNameBindings.length; i++) {
            if (!isUnbound(i) && varName.equals(this.parameterNameBindings[i])) {
                return true;
            }
        }
        return false;
    }

    /**
     * 是否符合java变量命名
     *
     * @param candidateToken
     * @return
     */
    private String maybeExtractVariableName(String candidateToken) {
        if (AspectJProxyUtils.isVariableName(candidateToken)) {
            return candidateToken;
        }
        return null;
    }

    private PointcutBody getPointcutBody(String[] tokens, int startIndex) {
        int numTokensConsumed = 0;
        String currentToken = tokens[startIndex];
        int bodyStart = currentToken.indexOf('(');
        /**
         * 只有左括号，则全部都匹配
         */
        if (currentToken.charAt(currentToken.length() - 1) == ')') {
            return new PointcutBody(0, currentToken.substring(bodyStart + 1, currentToken.length() - 1));
        } else {
            StringBuilder sb = new StringBuilder();
            /**
             * 左括号存在并且不是最后一个
             */
            if (bodyStart >= 0 && bodyStart != (currentToken.length() - 1)) {
                sb.append(currentToken.substring(bodyStart + 1));
                sb.append(' ');
            }
            numTokensConsumed++;
            int currentIndex = startIndex + numTokensConsumed;
            while (currentIndex < tokens.length) {
                /**
                 * 左括号，跳过
                 */
                if (tokens[currentIndex].equals("(")) {
                    currentIndex++;
                    continue;
                }

                /**
                 * 遇到右括号，结束
                 */
                if (tokens[currentIndex].endsWith(")")) {
                    sb.append(tokens[currentIndex], 0, tokens[currentIndex].length() - 1);
                    return new PointcutBody(numTokensConsumed, sb.toString().trim());
                }

                String toAppend = tokens[currentIndex];
                if (toAppend.startsWith("(")) {
                    toAppend = toAppend.substring(1);
                }
                sb.append(toAppend);
                sb.append(' ');
                currentIndex++;
                numTokensConsumed++;
            }

        }

        // 查找失败
        return new PointcutBody(numTokensConsumed, null);
    }

    /**
     * 规定：第一个joinpoint，第二个异常，后边就是返回值
     * 返回值不知道类型，无法根据类型匹配
     *
     * @return
     */
    private boolean maybeBindReturningVariable() {
        if (this.returningName != null) {
            int index = -1;
            for (int i = 0; i < this.argumentTypes.length; i++) {
                if (this.argumentTypes[i] == null) {
                    index = i;
                    break;
                }
            }
            if (index == -1) {
                throw new IllegalStateException("Binding of throwing parameter '" + this.throwingName
                        + "' could not be completed as no available arguments are a subtype of Throwable");
            }
            bindParameterName(index, returningName);
            return true;
        }
        return false;
    }

    private boolean maybeBindThrowingVariable() {
        if (this.throwingName != null) {
            int index = -1;
            for (int i = 0; i < this.argumentTypes.length; i++) {
                if (isUnbound(i) && isSubtypeOf(Throwable.class, i)) {
                    index = i;
                    break;
                }
            }
            if (index == -1) {
                throw new IllegalStateException("Binding of throwing parameter '" + this.throwingName
                        + "' could not be completed as no available arguments are a subtype of Throwable");
            }
            bindParameterName(index, throwingName);
            return true;
        }
        return false;
    }

    private boolean isSubtypeOf(Class<?> clazz, int i) {
        return clazz.isAssignableFrom(this.argumentTypes[i]);
    }

    private boolean maybeBindThisJoinPointStaticPart() {
        Class<?> argumentType = this.argumentTypes[0];
        if (argumentType == JoinPoint.StaticPart.class) {
            bindParameterName(0, THIS_JOIN_POINT_STATIC_PART);
            return true;
        }
        return false;
    }

    /**
     * 绑定joinPoint参数
     *
     * @return
     */
    private boolean maybeBindThisJoinPoint() {
        Class<?> argumentType = this.argumentTypes[0];
        if (argumentType == JoinPoint.class || argumentType == ProceedingJoinPoint.class) {
            bindParameterName(0, THIS_JOIN_POINT);
            return true;
        }
        return false;
    }

    private void bindParameterName(int index, String name) {
        this.parameterNameBindings[index] = name;
        this.numberOfRemainingUnboundArguments--;
    }

    private int countNumberOfUnboundPrimitiveArguments() {
        int count = 0;
        for (int i = 0; i < this.argumentTypes.length; i++) {
            if (isUnbound(i) && this.argumentTypes[i].isPrimitive()) {
                count++;
            }
        }
        return count;
    }

    /**
     * 未绑定槽位
     *
     * @param i
     * @return
     */
    private boolean isUnbound(int i) {
        return this.parameterNameBindings[i] == null;
    }

    @Override
    public String[] getParameterNames(Constructor<?> ctor) {
        /**
         * 不支持构造器匹配
         */
        if (this.raiseExceptions) {
            throw new UnsupportedOperationException("An advice method can never be a constructor");
        } else {
            return null;
        }
    }


    /**
     * 切点表达式匹配到的信息
     */
    private static class PointcutBody {

        /**
         * 下标
         */
        private int numTokensConsumed;

        /**
         * 内容
         */
        private String text;

        public PointcutBody(int tokens, String text) {
            this.numTokensConsumed = tokens;
            this.text = text;
        }
    }
}
